/*!
 * # Semantic UI - Form Validation
 * http://github.com/semantic-org/semantic-ui/
 *
 *
 * Released under the MIT license
 * http://opensource.org/licenses/MIT
 *
 */

(function($, window, document, undefined) {
  "use strict";

  window =
    typeof window != "undefined" && window.Math == Math
      ? window
      : typeof self != "undefined" && self.Math == Math
        ? self
        : Function("return this")();

  $.fn.form = function(parameters) {
    var $allModules = $(this),
      moduleSelector = $allModules.selector || "",
      time = new Date().getTime(),
      performance = [],
      query = arguments[0],
      legacyParameters = arguments[1],
      methodInvoked = typeof query == "string",
      queryArguments = [].slice.call(arguments, 1),
      returnedValue;
    $allModules.each(function() {
      var $module = $(this),
        element = this,
        formErrors = [],
        keyHeldDown = false,
        // set at run-time
        $field,
        $group,
        $message,
        $prompt,
        $submit,
        $clear,
        $reset,
        settings,
        validation,
        metadata,
        selector,
        className,
        regExp,
        error,
        namespace,
        moduleNamespace,
        eventNamespace,
        instance,
        module;

      module = {
        initialize: function() {
          // settings grabbed at run time
          module.get.settings();
          if (methodInvoked) {
            if (instance === undefined) {
              module.instantiate();
            }
            module.invoke(query);
          } else {
            if (instance !== undefined) {
              instance.invoke("destroy");
            }
            module.verbose("Initializing form validation", $module, settings);
            module.bindEvents();
            module.set.defaults();
            module.instantiate();
          }
        },

        instantiate: function() {
          module.verbose("Storing instance of module", module);
          instance = module;
          $module.data(moduleNamespace, module);
        },

        destroy: function() {
          module.verbose("Destroying previous module", instance);
          module.removeEvents();
          $module.removeData(moduleNamespace);
        },

        refresh: function() {
          module.verbose("Refreshing selector cache");
          $field = $module.find(selector.field);
          $group = $module.find(selector.group);
          $message = $module.find(selector.message);
          $prompt = $module.find(selector.prompt);

          $submit = $module.find(selector.submit);
          $clear = $module.find(selector.clear);
          $reset = $module.find(selector.reset);
        },

        submit: function() {
          module.verbose("Submitting form", $module);
          $module.submit();
        },

        attachEvents: function(selector, action) {
          action = action || "submit";
          $(selector).on("click" + eventNamespace, function(event) {
            module[action]();
            event.preventDefault();
          });
        },

        bindEvents: function() {
          module.verbose("Attaching form events");
          $module
            .on("submit" + eventNamespace, module.validate.form)
            .on(
              "blur" + eventNamespace,
              selector.field,
              module.event.field.blur
            )
            .on("click" + eventNamespace, selector.submit, module.submit)
            .on("click" + eventNamespace, selector.reset, module.reset)
            .on("click" + eventNamespace, selector.clear, module.clear);
          if (settings.keyboardShortcuts) {
            $module.on(
              "keydown" + eventNamespace,
              selector.field,
              module.event.field.keydown
            );
          }
          $field.each(function() {
            var $input = $(this),
              type = $input.prop("type"),
              inputEvent = module.get.changeEvent(type, $input);
            $(this).on(inputEvent + eventNamespace, module.event.field.change);
          });
        },

        clear: function() {
          $field.each(function() {
            var $field = $(this),
              $element = $field.parent(),
              $fieldGroup = $field.closest($group),
              $prompt = $fieldGroup.find(selector.prompt),
              defaultValue = $field.data(metadata.defaultValue) || "",
              isCheckbox = $element.is(selector.uiCheckbox),
              isDropdown = $element.is(selector.uiDropdown),
              isErrored = $fieldGroup.hasClass(className.error);
            if (isErrored) {
              module.verbose("Resetting error on field", $fieldGroup);
              $fieldGroup.removeClass(className.error);
              $prompt.remove();
            }
            if (isDropdown) {
              module.verbose(
                "Resetting dropdown value",
                $element,
                defaultValue
              );
              $element.dropdown("clear");
            } else if (isCheckbox) {
              $field.prop("checked", false);
            } else {
              module.verbose("Resetting field value", $field, defaultValue);
              $field.val("");
            }
          });
        },

        reset: function() {
          $field.each(function() {
            var $field = $(this),
              $element = $field.parent(),
              $fieldGroup = $field.closest($group),
              $prompt = $fieldGroup.find(selector.prompt),
              defaultValue = $field.data(metadata.defaultValue),
              isCheckbox = $element.is(selector.uiCheckbox),
              isDropdown = $element.is(selector.uiDropdown),
              isErrored = $fieldGroup.hasClass(className.error);
            if (defaultValue === undefined) {
              return;
            }
            if (isErrored) {
              module.verbose("Resetting error on field", $fieldGroup);
              $fieldGroup.removeClass(className.error);
              $prompt.remove();
            }
            if (isDropdown) {
              module.verbose(
                "Resetting dropdown value",
                $element,
                defaultValue
              );
              $element.dropdown("restore defaults");
            } else if (isCheckbox) {
              module.verbose(
                "Resetting checkbox value",
                $element,
                defaultValue
              );
              $field.prop("checked", defaultValue);
            } else {
              module.verbose("Resetting field value", $field, defaultValue);
              $field.val(defaultValue);
            }
          });
        },

        determine: {
          isValid: function() {
            var allValid = true;
            $.each(validation, function(fieldName, field) {
              if (!module.validate.field(field, fieldName, true)) {
                allValid = false;
              }
            });
            return allValid;
          }
        },

        is: {
          bracketedRule: function(rule) {
            return rule.type && rule.type.match(settings.regExp.bracket);
          },
          shorthandFields: function(fields) {
            var fieldKeys = Object.keys(fields),
              firstRule = fields[fieldKeys[0]];
            return module.is.shorthandRules(firstRule);
          },
          // duck type rule test
          shorthandRules: function(rules) {
            return typeof rules == "string" || $.isArray(rules);
          },
          empty: function($field) {
            if (!$field || $field.length === 0) {
              return true;
            } else if ($field.is('input[type="checkbox"]')) {
              return !$field.is(":checked");
            } else {
              return module.is.blank($field);
            }
          },
          blank: function($field) {
            return $.trim($field.val()) === "";
          },
          valid: function(field) {
            var allValid = true;
            if (field) {
              module.verbose("Checking if field is valid", field);
              return module.validate.field(validation[field], field, false);
            } else {
              module.verbose("Checking if form is valid");
              $.each(validation, function(fieldName, field) {
                if (!module.is.valid(fieldName)) {
                  allValid = false;
                }
              });
              return allValid;
            }
          }
        },

        removeEvents: function() {
          $module.off(eventNamespace);
          $field.off(eventNamespace);
          $submit.off(eventNamespace);
          $field.off(eventNamespace);
        },

        event: {
          field: {
            keydown: function(event) {
              var $field = $(this),
                key = event.which,
                isInput = $field.is(selector.input),
                isCheckbox = $field.is(selector.checkbox),
                isInDropdown = $field.closest(selector.uiDropdown).length > 0,
                keyCode = {
                  enter: 13,
                  escape: 27
                };
              if (key == keyCode.escape) {
                module.verbose("Escape key pressed blurring field");
                $field.blur();
              }
              if (
                !event.ctrlKey &&
                key == keyCode.enter &&
                isInput &&
                !isInDropdown &&
                !isCheckbox
              ) {
                if (!keyHeldDown) {
                  $field.one(
                    "keyup" + eventNamespace,
                    module.event.field.keyup
                  );
                  module.submit();
                  module.debug("Enter pressed on input submitting form");
                }
                keyHeldDown = true;
              }
            },
            keyup: function() {
              keyHeldDown = false;
            },
            blur: function(event) {
              var $field = $(this),
                $fieldGroup = $field.closest($group),
                validationRules = module.get.validation($field);
              if ($fieldGroup.hasClass(className.error)) {
                module.debug("Revalidating field", $field, validationRules);
                if (validationRules) {
                  module.validate.field(validationRules);
                }
              } else if (settings.on == "blur") {
                if (validationRules) {
                  module.validate.field(validationRules);
                }
              }
            },
            change: function(event) {
              var $field = $(this),
                $fieldGroup = $field.closest($group),
                validationRules = module.get.validation($field);
              if (
                validationRules &&
                (settings.on == "change" ||
                  ($fieldGroup.hasClass(className.error) &&
                    settings.revalidate))
              ) {
                clearTimeout(module.timer);
                module.timer = setTimeout(function() {
                  module.debug(
                    "Revalidating field",
                    $field,
                    module.get.validation($field)
                  );
                  module.validate.field(validationRules);
                }, settings.delay);
              }
            }
          }
        },

        get: {
          ancillaryValue: function(rule) {
            if (!rule.type || (!rule.value && !module.is.bracketedRule(rule))) {
              return false;
            }
            return rule.value !== undefined
              ? rule.value
              : rule.type.match(settings.regExp.bracket)[1] + "";
          },
          ruleName: function(rule) {
            if (module.is.bracketedRule(rule)) {
              return rule.type.replace(
                rule.type.match(settings.regExp.bracket)[0],
                ""
              );
            }
            return rule.type;
          },
          changeEvent: function(type, $input) {
            if (
              type == "checkbox" ||
              type == "radio" ||
              type == "hidden" ||
              $input.is("select")
            ) {
              return "change";
            } else {
              return module.get.inputEvent();
            }
          },
          inputEvent: function() {
            return document.createElement("input").oninput !== undefined
              ? "input"
              : document.createElement("input").onpropertychange !== undefined
                ? "propertychange"
                : "keyup";
          },
          fieldsFromShorthand: function(fields) {
            var fullFields = {};
            $.each(fields, function(name, rules) {
              if (typeof rules == "string") {
                rules = [rules];
              }
              fullFields[name] = {
                rules: []
              };
              $.each(rules, function(index, rule) {
                fullFields[name].rules.push({ type: rule });
              });
            });
            return fullFields;
          },
          prompt: function(rule, field) {
            var ruleName = module.get.ruleName(rule),
              ancillary = module.get.ancillaryValue(rule),
              $field = module.get.field(field.identifier),
              value = $field.val(),
              prompt = $.isFunction(rule.prompt)
                ? rule.prompt(value)
                : rule.prompt ||
                  settings.prompt[ruleName] ||
                  settings.text.unspecifiedRule,
              requiresValue = prompt.search("{value}") !== -1,
              requiresName = prompt.search("{name}") !== -1,
              $label,
              name;
            if (requiresValue) {
              prompt = prompt.replace("{value}", $field.val());
            }
            if (requiresName) {
              $label = $field
                .closest(selector.group)
                .find("label")
                .eq(0);
              name =
                $label.length == 1
                  ? $label.text()
                  : $field.prop("placeholder") ||
                    settings.text.unspecifiedField;
              prompt = prompt.replace("{name}", name);
            }
            prompt = prompt.replace("{identifier}", field.identifier);
            prompt = prompt.replace("{ruleValue}", ancillary);
            if (!rule.prompt) {
              module.verbose(
                "Using default validation prompt for type",
                prompt,
                ruleName
              );
            }
            return prompt;
          },
          settings: function() {
            if ($.isPlainObject(parameters)) {
              var keys = Object.keys(parameters),
                isLegacySettings =
                  keys.length > 0
                    ? parameters[keys[0]].identifier !== undefined &&
                      parameters[keys[0]].rules !== undefined
                    : false,
                ruleKeys;
              if (isLegacySettings) {
                // 1.x (ducktyped)
                settings = $.extend(
                  true,
                  {},
                  $.fn.form.settings,
                  legacyParameters
                );
                validation = $.extend(
                  {},
                  $.fn.form.settings.defaults,
                  parameters
                );
                module.error(settings.error.oldSyntax, element);
                module.verbose(
                  "Extending settings from legacy parameters",
                  validation,
                  settings
                );
              } else {
                // 2.x
                if (
                  parameters.fields &&
                  module.is.shorthandFields(parameters.fields)
                ) {
                  parameters.fields = module.get.fieldsFromShorthand(
                    parameters.fields
                  );
                }
                settings = $.extend(true, {}, $.fn.form.settings, parameters);
                validation = $.extend(
                  {},
                  $.fn.form.settings.defaults,
                  settings.fields
                );
                module.verbose("Extending settings", validation, settings);
              }
            } else {
              settings = $.fn.form.settings;
              validation = $.fn.form.settings.defaults;
              module.verbose(
                "Using default form validation",
                validation,
                settings
              );
            }

            // shorthand
            namespace = settings.namespace;
            metadata = settings.metadata;
            selector = settings.selector;
            className = settings.className;
            regExp = settings.regExp;
            error = settings.error;
            moduleNamespace = "module-" + namespace;
            eventNamespace = "." + namespace;

            // grab instance
            instance = $module.data(moduleNamespace);

            // refresh selector cache
            module.refresh();
          },
          field: function(identifier) {
            module.verbose("Finding field with identifier", identifier);
            identifier = module.escape.string(identifier);
            if ($field.filter("#" + identifier).length > 0) {
              return $field.filter("#" + identifier);
            } else if (
              $field.filter('[name="' + identifier + '"]').length > 0
            ) {
              return $field.filter('[name="' + identifier + '"]');
            } else if (
              $field.filter('[name="' + identifier + '[]"]').length > 0
            ) {
              return $field.filter('[name="' + identifier + '[]"]');
            } else if (
              $field.filter(
                "[data-" + metadata.validate + '="' + identifier + '"]'
              ).length > 0
            ) {
              return $field.filter(
                "[data-" + metadata.validate + '="' + identifier + '"]'
              );
            }
            return $("<input/>");
          },
          fields: function(fields) {
            var $fields = $();
            $.each(fields, function(index, name) {
              $fields = $fields.add(module.get.field(name));
            });
            return $fields;
          },
          validation: function($field) {
            var fieldValidation, identifier;
            if (!validation) {
              return false;
            }
            $.each(validation, function(fieldName, field) {
              identifier = field.identifier || fieldName;
              if (module.get.field(identifier)[0] == $field[0]) {
                field.identifier = identifier;
                fieldValidation = field;
              }
            });
            return fieldValidation || false;
          },
          value: function(field) {
            var fields = [],
              results;
            fields.push(field);
            results = module.get.values.call(element, fields);
            return results[field];
          },
          values: function(fields) {
            var $fields = $.isArray(fields)
                ? module.get.fields(fields)
                : $field,
              values = {};
            $fields.each(function(index, field) {
              var $field = $(field),
                type = $field.prop("type"),
                name = $field.prop("name"),
                value = $field.val(),
                isCheckbox = $field.is(selector.checkbox),
                isRadio = $field.is(selector.radio),
                isMultiple = name.indexOf("[]") !== -1,
                isChecked = isCheckbox ? $field.is(":checked") : false;
              if (name) {
                if (isMultiple) {
                  name = name.replace("[]", "");
                  if (!values[name]) {
                    values[name] = [];
                  }
                  if (isCheckbox) {
                    if (isChecked) {
                      values[name].push(value || true);
                    } else {
                      values[name].push(false);
                    }
                  } else {
                    values[name].push(value);
                  }
                } else {
                  if (isRadio) {
                    if (values[name] === undefined || values[name] == false) {
                      values[name] = isChecked ? value || true : false;
                    }
                  } else if (isCheckbox) {
                    if (isChecked) {
                      values[name] = value || true;
                    } else {
                      values[name] = false;
                    }
                  } else {
                    values[name] = value;
                  }
                }
              }
            });
            return values;
          }
        },

        has: {
          field: function(identifier) {
            module.verbose(
              "Checking for existence of a field with identifier",
              identifier
            );
            identifier = module.escape.string(identifier);
            if (typeof identifier !== "string") {
              module.error(error.identifier, identifier);
            }
            if ($field.filter("#" + identifier).length > 0) {
              return true;
            } else if (
              $field.filter('[name="' + identifier + '"]').length > 0
            ) {
              return true;
            } else if (
              $field.filter(
                "[data-" + metadata.validate + '="' + identifier + '"]'
              ).length > 0
            ) {
              return true;
            }
            return false;
          }
        },

        escape: {
          string: function(text) {
            text = String(text);
            return text.replace(regExp.escape, "\\$&");
          }
        },

        add: {
          // alias
          rule: function(name, rules) {
            module.add.field(name, rules);
          },
          field: function(name, rules) {
            var newValidation = {};
            if (module.is.shorthandRules(rules)) {
              rules = $.isArray(rules) ? rules : [rules];
              newValidation[name] = {
                rules: []
              };
              $.each(rules, function(index, rule) {
                newValidation[name].rules.push({ type: rule });
              });
            } else {
              newValidation[name] = rules;
            }
            validation = $.extend({}, validation, newValidation);
            module.debug("Adding rules", newValidation, validation);
          },
          fields: function(fields) {
            var newValidation;
            if (fields && module.is.shorthandFields(fields)) {
              newValidation = module.get.fieldsFromShorthand(fields);
            } else {
              newValidation = fields;
            }
            validation = $.extend({}, validation, newValidation);
          },
          prompt: function(identifier, errors) {
            var $field = module.get.field(identifier),
              $fieldGroup = $field.closest($group),
              $prompt = $fieldGroup.children(selector.prompt),
              promptExists = $prompt.length !== 0;
            errors = typeof errors == "string" ? [errors] : errors;
            module.verbose("Adding field error state", identifier);
            $fieldGroup.addClass(className.error);
            if (settings.inline) {
              if (!promptExists) {
                $prompt = settings.templates.prompt(errors);
                $prompt.appendTo($fieldGroup);
              }
              $prompt.html(errors[0]);
              if (!promptExists) {
                if (
                  settings.transition &&
                  $.fn.transition !== undefined &&
                  $module.transition("is supported")
                ) {
                  module.verbose(
                    "Displaying error with css transition",
                    settings.transition
                  );
                  $prompt.transition(
                    settings.transition + " in",
                    settings.duration
                  );
                } else {
                  module.verbose(
                    "Displaying error with fallback javascript animation"
                  );
                  $prompt.fadeIn(settings.duration);
                }
              } else {
                module.verbose(
                  "Inline errors are disabled, no inline error added",
                  identifier
                );
              }
            }
          },
          errors: function(errors) {
            module.debug("Adding form error messages", errors);
            module.set.error();
            $message.html(settings.templates.error(errors));
          }
        },

        remove: {
          rule: function(field, rule) {
            var rules = $.isArray(rule) ? rule : [rule];
            if (rule == undefined) {
              module.debug("Removed all rules");
              validation[field].rules = [];
              return;
            }
            if (
              validation[field] == undefined ||
              !$.isArray(validation[field].rules)
            ) {
              return;
            }
            $.each(validation[field].rules, function(index, rule) {
              if (rules.indexOf(rule.type) !== -1) {
                module.debug("Removed rule", rule.type);
                validation[field].rules.splice(index, 1);
              }
            });
          },
          field: function(field) {
            var fields = $.isArray(field) ? field : [field];
            $.each(fields, function(index, field) {
              module.remove.rule(field);
            });
          },
          // alias
          rules: function(field, rules) {
            if ($.isArray(field)) {
              $.each(fields, function(index, field) {
                module.remove.rule(field, rules);
              });
            } else {
              module.remove.rule(field, rules);
            }
          },
          fields: function(fields) {
            module.remove.field(fields);
          },
          prompt: function(identifier) {
            var $field = module.get.field(identifier),
              $fieldGroup = $field.closest($group),
              $prompt = $fieldGroup.children(selector.prompt);
            $fieldGroup.removeClass(className.error);
            if (settings.inline && $prompt.is(":visible")) {
              module.verbose("Removing prompt for field", identifier);
              if (
                settings.transition &&
                $.fn.transition !== undefined &&
                $module.transition("is supported")
              ) {
                $prompt.transition(
                  settings.transition + " out",
                  settings.duration,
                  function() {
                    $prompt.remove();
                  }
                );
              } else {
                $prompt.fadeOut(settings.duration, function() {
                  $prompt.remove();
                });
              }
            }
          }
        },

        set: {
          success: function() {
            $module.removeClass(className.error).addClass(className.success);
          },
          defaults: function() {
            $field.each(function() {
              var $field = $(this),
                isCheckbox = $field.filter(selector.checkbox).length > 0,
                value = isCheckbox ? $field.is(":checked") : $field.val();
              $field.data(metadata.defaultValue, value);
            });
          },
          error: function() {
            $module.removeClass(className.success).addClass(className.error);
          },
          value: function(field, value) {
            var fields = {};
            fields[field] = value;
            return module.set.values.call(element, fields);
          },
          values: function(fields) {
            if ($.isEmptyObject(fields)) {
              return;
            }
            $.each(fields, function(key, value) {
              var $field = module.get.field(key),
                $element = $field.parent(),
                isMultiple = $.isArray(value),
                isCheckbox = $element.is(selector.uiCheckbox),
                isDropdown = $element.is(selector.uiDropdown),
                isRadio = $field.is(selector.radio) && isCheckbox,
                fieldExists = $field.length > 0,
                $multipleField;
              if (fieldExists) {
                if (isMultiple && isCheckbox) {
                  module.verbose("Selecting multiple", value, $field);
                  $element.checkbox("uncheck");
                  $.each(value, function(index, value) {
                    $multipleField = $field.filter('[value="' + value + '"]');
                    $element = $multipleField.parent();
                    if ($multipleField.length > 0) {
                      $element.checkbox("check");
                    }
                  });
                } else if (isRadio) {
                  module.verbose("Selecting radio value", value, $field);
                  $field
                    .filter('[value="' + value + '"]')
                    .parent(selector.uiCheckbox)
                    .checkbox("check");
                } else if (isCheckbox) {
                  module.verbose("Setting checkbox value", value, $element);
                  if (value === true) {
                    $element.checkbox("check");
                  } else {
                    $element.checkbox("uncheck");
                  }
                } else if (isDropdown) {
                  module.verbose("Setting dropdown value", value, $element);
                  $element.dropdown("set selected", value);
                } else {
                  module.verbose("Setting field value", value, $field);
                  $field.val(value);
                }
              }
            });
          }
        },

        validate: {
          form: function(event, ignoreCallbacks) {
            var values = module.get.values(),
              apiRequest;

            // input keydown event will fire submit repeatedly by browser default
            if (keyHeldDown) {
              return false;
            }

            // reset errors
            formErrors = [];
            if (module.determine.isValid()) {
              module.debug("Form has no validation errors, submitting");
              module.set.success();
              if (ignoreCallbacks !== true) {
                return settings.onSuccess.call(element, event, values);
              }
            } else {
              module.debug("Form has errors");
              module.set.error();
              if (!settings.inline) {
                module.add.errors(formErrors);
              }
              // prevent ajax submit
              if ($module.data("moduleApi") !== undefined) {
                event.stopImmediatePropagation();
              }
              if (ignoreCallbacks !== true) {
                return settings.onFailure.call(element, formErrors, values);
              }
            }
          },

          // takes a validation object and returns whether field passes validation
          field: function(field, fieldName, showErrors) {
            showErrors = showErrors !== undefined ? showErrors : true;
            if (typeof field == "string") {
              module.verbose("Validating field", field);
              fieldName = field;
              field = validation[field];
            }
            var identifier = field.identifier || fieldName,
              $field = module.get.field(identifier),
              $dependsField = field.depends
                ? module.get.field(field.depends)
                : false,
              fieldValid = true,
              fieldErrors = [];
            if (!field.identifier) {
              module.debug("Using field name as identifier", identifier);
              field.identifier = identifier;
            }
            if ($field.prop("disabled")) {
              module.debug("Field is disabled. Skipping", identifier);
              fieldValid = true;
            } else if (field.optional && module.is.blank($field)) {
              module.debug("Field is optional and blank. Skipping", identifier);
              fieldValid = true;
            } else if (field.depends && module.is.empty($dependsField)) {
              module.debug(
                "Field depends on another value that is not present or empty. Skipping",
                $dependsField
              );
              fieldValid = true;
            } else if (field.rules !== undefined) {
              $.each(field.rules, function(index, rule) {
                if (
                  module.has.field(identifier) &&
                  !module.validate.rule(field, rule)
                ) {
                  module.debug("Field is invalid", identifier, rule.type);
                  fieldErrors.push(module.get.prompt(rule, field));
                  fieldValid = false;
                }
              });
            }
            if (fieldValid) {
              if (showErrors) {
                module.remove.prompt(identifier, fieldErrors);
                settings.onValid.call($field);
              }
            } else {
              if (showErrors) {
                formErrors = formErrors.concat(fieldErrors);
                module.add.prompt(identifier, fieldErrors);
                settings.onInvalid.call($field, fieldErrors);
              }
              return false;
            }
            return true;
          },

          // takes validation rule and returns whether field passes rule
          rule: function(field, rule) {
            var $field = module.get.field(field.identifier),
              type = rule.type,
              value = $field.val(),
              isValid = true,
              ancillary = module.get.ancillaryValue(rule),
              ruleName = module.get.ruleName(rule),
              ruleFunction = settings.rules[ruleName];
            if (!$.isFunction(ruleFunction)) {
              module.error(error.noRule, ruleName);
              return;
            }
            // cast to string avoiding encoding special values
            value =
              value === undefined || value === "" || value === null
                ? ""
                : $.trim(value + "");
            return ruleFunction.call($field, value, ancillary);
          }
        },

        setting: function(name, value) {
          if ($.isPlainObject(name)) {
            $.extend(true, settings, name);
          } else if (value !== undefined) {
            settings[name] = value;
          } else {
            return settings[name];
          }
        },
        internal: function(name, value) {
          if ($.isPlainObject(name)) {
            $.extend(true, module, name);
          } else if (value !== undefined) {
            module[name] = value;
          } else {
            return module[name];
          }
        },
        debug: function() {
          if (!settings.silent && settings.debug) {
            if (settings.performance) {
              module.performance.log(arguments);
            } else {
              module.debug = Function.prototype.bind.call(
                console.info,
                console,
                settings.name + ":"
              );
              module.debug.apply(console, arguments);
            }
          }
        },
        verbose: function() {
          if (!settings.silent && settings.verbose && settings.debug) {
            if (settings.performance) {
              module.performance.log(arguments);
            } else {
              module.verbose = Function.prototype.bind.call(
                console.info,
                console,
                settings.name + ":"
              );
              module.verbose.apply(console, arguments);
            }
          }
        },
        error: function() {
          if (!settings.silent) {
            module.error = Function.prototype.bind.call(
              console.error,
              console,
              settings.name + ":"
            );
            module.error.apply(console, arguments);
          }
        },
        performance: {
          log: function(message) {
            var currentTime, executionTime, previousTime;
            if (settings.performance) {
              currentTime = new Date().getTime();
              previousTime = time || currentTime;
              executionTime = currentTime - previousTime;
              time = currentTime;
              performance.push({
                Name: message[0],
                Arguments: [].slice.call(message, 1) || "",
                Element: element,
                "Execution Time": executionTime
              });
            }
            clearTimeout(module.performance.timer);
            module.performance.timer = setTimeout(
              module.performance.display,
              500
            );
          },
          display: function() {
            var title = settings.name + ":",
              totalTime = 0;
            time = false;
            clearTimeout(module.performance.timer);
            $.each(performance, function(index, data) {
              totalTime += data["Execution Time"];
            });
            title += " " + totalTime + "ms";
            if (moduleSelector) {
              title += " '" + moduleSelector + "'";
            }
            if ($allModules.length > 1) {
              title += " " + "(" + $allModules.length + ")";
            }
            if (
              (console.group !== undefined || console.table !== undefined) &&
              performance.length > 0
            ) {
              console.groupCollapsed(title);
              if (console.table) {
                console.table(performance);
              } else {
                $.each(performance, function(index, data) {
                  console.log(
                    data["Name"] + ": " + data["Execution Time"] + "ms"
                  );
                });
              }
              console.groupEnd();
            }
            performance = [];
          }
        },
        invoke: function(query, passedArguments, context) {
          var object = instance,
            maxDepth,
            found,
            response;
          passedArguments = passedArguments || queryArguments;
          context = element || context;
          if (typeof query == "string" && object !== undefined) {
            query = query.split(/[\. ]/);
            maxDepth = query.length - 1;
            $.each(query, function(depth, value) {
              var camelCaseValue =
                depth != maxDepth
                  ? value +
                    query[depth + 1].charAt(0).toUpperCase() +
                    query[depth + 1].slice(1)
                  : query;
              if (
                $.isPlainObject(object[camelCaseValue]) &&
                depth != maxDepth
              ) {
                object = object[camelCaseValue];
              } else if (object[camelCaseValue] !== undefined) {
                found = object[camelCaseValue];
                return false;
              } else if ($.isPlainObject(object[value]) && depth != maxDepth) {
                object = object[value];
              } else if (object[value] !== undefined) {
                found = object[value];
                return false;
              } else {
                return false;
              }
            });
          }
          if ($.isFunction(found)) {
            response = found.apply(context, passedArguments);
          } else if (found !== undefined) {
            response = found;
          }
          if ($.isArray(returnedValue)) {
            returnedValue.push(response);
          } else if (returnedValue !== undefined) {
            returnedValue = [returnedValue, response];
          } else if (response !== undefined) {
            returnedValue = response;
          }
          return found;
        }
      };
      module.initialize();
    });

    return returnedValue !== undefined ? returnedValue : this;
  };

  $.fn.form.settings = {
    name: "Form",
    namespace: "form",

    debug: false,
    verbose: false,
    performance: true,

    fields: false,

    keyboardShortcuts: true,
    on: "submit",
    inline: false,

    delay: 200,
    revalidate: true,

    transition: "scale",
    duration: 200,

    onValid: function() {},
    onInvalid: function() {},
    onSuccess: function() {
      return true;
    },
    onFailure: function() {
      return false;
    },

    metadata: {
      defaultValue: "default",
      validate: "validate"
    },

    regExp: {
      htmlID: /^[a-zA-Z][\w:.-]*$/g,
      bracket: /\[(.*)\]/i,
      decimal: /^\d+\.?\d*$/,
      email: /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i,
      escape: /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,
      flags: /^\/(.*)\/(.*)?/,
      integer: /^\-?\d+$/,
      number: /^\-?\d*(\.\d+)?$/,
      url: /(https?:\/\/(?:www\.|(?!www))[^\s\.]+\.[^\s]{2,}|www\.[^\s]+\.[^\s]{2,})/i
    },

    text: {
      unspecifiedRule: "Please enter a valid value",
      unspecifiedField: "This field"
    },

    prompt: {
      empty: "{name} must have a value",
      checked: "{name} must be checked",
      email: "{name} must be a valid e-mail",
      url: "{name} must be a valid url",
      regExp: "{name} is not formatted correctly",
      integer: "{name} must be an integer",
      decimal: "{name} must be a decimal number",
      number: "{name} must be set to a number",
      is: '{name} must be "{ruleValue}"',
      isExactly: '{name} must be exactly "{ruleValue}"',
      not: '{name} cannot be set to "{ruleValue}"',
      notExactly: '{name} cannot be set to exactly "{ruleValue}"',
      contain: '{name} must contain "{ruleValue}"',
      containExactly: '{name} must contain exactly "{ruleValue}"',
      doesntContain: '{name} cannot contain  "{ruleValue}"',
      doesntContainExactly: '{name} cannot contain exactly "{ruleValue}"',
      minLength: "{name} must be at least {ruleValue} characters",
      length: "{name} must be at least {ruleValue} characters",
      exactLength: "{name} must be exactly {ruleValue} characters",
      maxLength: "{name} cannot be longer than {ruleValue} characters",
      match: "{name} must match {ruleValue} field",
      different: "{name} must have a different value than {ruleValue} field",
      creditCard: "{name} must be a valid credit card number",
      minCount: "{name} must have at least {ruleValue} choices",
      exactCount: "{name} must have exactly {ruleValue} choices",
      maxCount: "{name} must have {ruleValue} or less choices"
    },

    selector: {
      checkbox: 'input[type="checkbox"], input[type="radio"]',
      clear: ".clear",
      field: "input, textarea, select",
      group: ".field",
      input: "input",
      message: ".error.message",
      prompt: ".prompt.label",
      radio: 'input[type="radio"]',
      reset: '.reset:not([type="reset"])',
      submit: '.submit:not([type="submit"])',
      uiCheckbox: ".ui.checkbox",
      uiDropdown: ".ui.dropdown"
    },

    className: {
      error: "error",
      label: "ui prompt label",
      pressed: "down",
      success: "success"
    },

    error: {
      identifier: "You must specify a string identifier for each field",
      method: "The method you called is not defined.",
      noRule: "There is no rule matching the one you specified",
      oldSyntax:
        "Starting in 2.0 forms now only take a single settings object. Validation settings converted to new syntax automatically."
    },

    templates: {
      // template that produces error message
      error: function(errors) {
        var html = '<ul class="list">';
        $.each(errors, function(index, value) {
          html += "<li>" + value + "</li>";
        });
        html += "</ul>";
        return $(html);
      },

      // template that produces label
      prompt: function(errors) {
        return $("<div/>")
          .addClass("ui basic red pointing prompt label")
          .html(errors[0]);
      }
    },

    rules: {
      // is not empty or blank string
      empty: function(value) {
        return !(
          value === undefined ||
          "" === value ||
          ($.isArray(value) && value.length === 0)
        );
      },

      // checkbox checked
      checked: function() {
        return $(this).filter(":checked").length > 0;
      },

      // is most likely an email
      email: function(value) {
        return $.fn.form.settings.regExp.email.test(value);
      },

      // value is most likely url
      url: function(value) {
        return $.fn.form.settings.regExp.url.test(value);
      },

      // matches specified regExp
      regExp: function(value, regExp) {
        if (regExp instanceof RegExp) {
          return value.match(regExp);
        }
        var regExpParts = regExp.match($.fn.form.settings.regExp.flags),
          flags;
        // regular expression specified as /baz/gi (flags)
        if (regExpParts) {
          regExp = regExpParts.length >= 2 ? regExpParts[1] : regExp;
          flags = regExpParts.length >= 3 ? regExpParts[2] : "";
        }
        return value.match(new RegExp(regExp, flags));
      },

      // is valid integer or matches range
      integer: function(value, range) {
        var intRegExp = $.fn.form.settings.regExp.integer,
          min,
          max,
          parts;
        if (!range || ["", ".."].indexOf(range) !== -1) {
          // do nothing
        } else if (range.indexOf("..") == -1) {
          if (intRegExp.test(range)) {
            min = max = range - 0;
          }
        } else {
          parts = range.split("..", 2);
          if (intRegExp.test(parts[0])) {
            min = parts[0] - 0;
          }
          if (intRegExp.test(parts[1])) {
            max = parts[1] - 0;
          }
        }
        return (
          intRegExp.test(value) &&
          (min === undefined || value >= min) &&
          (max === undefined || value <= max)
        );
      },

      // is valid number (with decimal)
      decimal: function(value) {
        return $.fn.form.settings.regExp.decimal.test(value);
      },

      // is valid number
      number: function(value) {
        return $.fn.form.settings.regExp.number.test(value);
      },

      // is value (case insensitive)
      is: function(value, text) {
        text = typeof text == "string" ? text.toLowerCase() : text;
        value = typeof value == "string" ? value.toLowerCase() : value;
        return value == text;
      },

      // is value
      isExactly: function(value, text) {
        return value == text;
      },

      // value is not another value (case insensitive)
      not: function(value, notValue) {
        value = typeof value == "string" ? value.toLowerCase() : value;
        notValue =
          typeof notValue == "string" ? notValue.toLowerCase() : notValue;
        return value != notValue;
      },

      // value is not another value (case sensitive)
      notExactly: function(value, notValue) {
        return value != notValue;
      },

      // value contains text (insensitive)
      contains: function(value, text) {
        // escape regex characters
        text = text.replace($.fn.form.settings.regExp.escape, "\\$&");
        return value.search(new RegExp(text, "i")) !== -1;
      },

      // value contains text (case sensitive)
      containsExactly: function(value, text) {
        // escape regex characters
        text = text.replace($.fn.form.settings.regExp.escape, "\\$&");
        return value.search(new RegExp(text)) !== -1;
      },

      // value contains text (insensitive)
      doesntContain: function(value, text) {
        // escape regex characters
        text = text.replace($.fn.form.settings.regExp.escape, "\\$&");
        return value.search(new RegExp(text, "i")) === -1;
      },

      // value contains text (case sensitive)
      doesntContainExactly: function(value, text) {
        // escape regex characters
        text = text.replace($.fn.form.settings.regExp.escape, "\\$&");
        return value.search(new RegExp(text)) === -1;
      },

      // is at least string length
      minLength: function(value, requiredLength) {
        return value !== undefined ? value.length >= requiredLength : false;
      },

      // see rls notes for 2.0.6 (this is a duplicate of minLength)
      length: function(value, requiredLength) {
        return value !== undefined ? value.length >= requiredLength : false;
      },

      // is exactly length
      exactLength: function(value, requiredLength) {
        return value !== undefined ? value.length == requiredLength : false;
      },

      // is less than length
      maxLength: function(value, maxLength) {
        return value !== undefined ? value.length <= maxLength : false;
      },

      // matches another field
      match: function(value, identifier) {
        var $form = $(this),
          matchingValue;
        if ($('[data-validate="' + identifier + '"]').length > 0) {
          matchingValue = $('[data-validate="' + identifier + '"]').val();
        } else if ($("#" + identifier).length > 0) {
          matchingValue = $("#" + identifier).val();
        } else if ($('[name="' + identifier + '"]').length > 0) {
          matchingValue = $('[name="' + identifier + '"]').val();
        } else if ($('[name="' + identifier + '[]"]').length > 0) {
          matchingValue = $('[name="' + identifier + '[]"]');
        }
        return matchingValue !== undefined
          ? value.toString() == matchingValue.toString()
          : false;
      },

      // different than another field
      different: function(value, identifier) {
        // use either id or name of field
        var $form = $(this),
          matchingValue;
        if ($('[data-validate="' + identifier + '"]').length > 0) {
          matchingValue = $('[data-validate="' + identifier + '"]').val();
        } else if ($("#" + identifier).length > 0) {
          matchingValue = $("#" + identifier).val();
        } else if ($('[name="' + identifier + '"]').length > 0) {
          matchingValue = $('[name="' + identifier + '"]').val();
        } else if ($('[name="' + identifier + '[]"]').length > 0) {
          matchingValue = $('[name="' + identifier + '[]"]');
        }
        return matchingValue !== undefined
          ? value.toString() !== matchingValue.toString()
          : false;
      },

      creditCard: function(cardNumber, cardTypes) {
        var cards = {
            visa: {
              pattern: /^4/,
              length: [16]
            },
            amex: {
              pattern: /^3[47]/,
              length: [15]
            },
            mastercard: {
              pattern: /^5[1-5]/,
              length: [16]
            },
            discover: {
              pattern: /^(6011|622(12[6-9]|1[3-9][0-9]|[2-8][0-9]{2}|9[0-1][0-9]|92[0-5]|64[4-9])|65)/,
              length: [16]
            },
            unionPay: {
              pattern: /^(62|88)/,
              length: [16, 17, 18, 19]
            },
            jcb: {
              pattern: /^35(2[89]|[3-8][0-9])/,
              length: [16]
            },
            maestro: {
              pattern: /^(5018|5020|5038|6304|6759|676[1-3])/,
              length: [12, 13, 14, 15, 16, 17, 18, 19]
            },
            dinersClub: {
              pattern: /^(30[0-5]|^36)/,
              length: [14]
            },
            laser: {
              pattern: /^(6304|670[69]|6771)/,
              length: [16, 17, 18, 19]
            },
            visaElectron: {
              pattern: /^(4026|417500|4508|4844|491(3|7))/,
              length: [16]
            }
          },
          valid = {},
          validCard = false,
          requiredTypes =
            typeof cardTypes == "string" ? cardTypes.split(",") : false,
          unionPay,
          validation;

        if (typeof cardNumber !== "string" || cardNumber.length === 0) {
          return;
        }

        // allow dashes in card
        cardNumber = cardNumber.replace(/[\-]/g, "");

        // verify card types
        if (requiredTypes) {
          $.each(requiredTypes, function(index, type) {
            // verify each card type
            validation = cards[type];
            if (validation) {
              valid = {
                length: $.inArray(cardNumber.length, validation.length) !== -1,
                pattern: cardNumber.search(validation.pattern) !== -1
              };
              if (valid.length && valid.pattern) {
                validCard = true;
              }
            }
          });

          if (!validCard) {
            return false;
          }
        }

        // skip luhn for UnionPay
        unionPay = {
          number: $.inArray(cardNumber.length, cards.unionPay.length) !== -1,
          pattern: cardNumber.search(cards.unionPay.pattern) !== -1
        };
        if (unionPay.number && unionPay.pattern) {
          return true;
        }

        // verify luhn, adapted from  <https://gist.github.com/2134376>
        var length = cardNumber.length,
          multiple = 0,
          producedValue = [
            [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
            [0, 2, 4, 6, 8, 1, 3, 5, 7, 9]
          ],
          sum = 0;
        while (length--) {
          sum +=
            producedValue[multiple][parseInt(cardNumber.charAt(length), 10)];
          multiple ^= 1;
        }
        return sum % 10 === 0 && sum > 0;
      },

      minCount: function(value, minCount) {
        if (minCount == 0) {
          return true;
        }
        if (minCount == 1) {
          return value !== "";
        }
        return value.split(",").length >= minCount;
      },

      exactCount: function(value, exactCount) {
        if (exactCount == 0) {
          return value === "";
        }
        if (exactCount == 1) {
          return value !== "" && value.search(",") === -1;
        }
        return value.split(",").length == exactCount;
      },

      maxCount: function(value, maxCount) {
        if (maxCount == 0) {
          return false;
        }
        if (maxCount == 1) {
          return value.search(",") === -1;
        }
        return value.split(",").length <= maxCount;
      }
    }
  };
})(jQuery, window, document);
